package main

import (
	"bytes"
	"fmt"
	"os/exec"
	"path/filepath"
	"strings"
	"text/template"
	"time"

	"github.com/gotomicro/ego/internal/tools"
	"google.golang.org/protobuf/compiler/protogen"
)

var importTestPackages = map[string]string{
	"testing":                               "",
	"context":                               "",
	"errors":                                "",
	"github.com/stretchr/testify/assert":    "",
	"google.golang.org/grpc":                "",
	"google.golang.org/protobuf/proto":      "",
	"github.com/gotomicro/ego/client/egrpc": "cegrpc",
	"github.com/gotomicro/ego/core/eerrors": "",
}

var importInitTestPackages = map[string]string{
	"net":                                   "",
	"context":                               "",
	"google.golang.org/grpc/test/bufconn":   "",
	"github.com/gotomicro/ego/server/egrpc": "",
}

func execCmd(name string, args ...string) (string, error) {
	cmd := exec.Command(name, args...)
	var outb, errb bytes.Buffer
	cmd.Stdout = &outb
	cmd.Stderr = &errb
	err := cmd.Run()
	if err != nil {
		return outb.String(), fmt.Errorf("error: %s, %w", errb.String(), err)
	}
	return strings.TrimSpace(outb.String()), nil
}

func genHiddenFile(f string) string {
	return filepath.Join(filepath.Dir(f), "."+filepath.Base(f))
}

type file struct {
	orig string
	temp string
	g    *protogen.GeneratedFile
}

func generateInitFile(gen *protogen.Plugin, f *protogen.File, out string, mod string) (*protogen.GeneratedFile, error) {
	cmdout, err := execCmd("go", "list", out)
	if err != nil {
		return nil, fmt.Errorf("get package path fail, %w", err)
	}
	outPkg := cmdout[strings.LastIndex(cmdout, "/")+1:]
	origFile := filepath.Join(out, "init_test.go")
	tempFile := genHiddenFile(origFile)
	tempG := gen.NewGeneratedFile(tempFile, "")
	tempG.P("// Code generated by protoc-gen-go-test. DO NOT EDIT.")
	tempG.P()
	tempG.P("package ", outPkg)
	tempG.P()
	tempG.P("import (")
	for path, name := range importInitTestPackages {
		tempG.P(fmt.Sprintf("%s \"%s\"", name, path))
	}
	tempG.P(")")
	tpl := template.New("init_test")
	buf := new(bytes.Buffer)
	err = template.Must(tpl.Parse(baseTpl)).Execute(buf, svcData{Time: time.Now().Format("2006-01-02T15:04:05")})
	if err != nil {
		return nil, fmt.Errorf("render tmpl fail, %w", err)
	}
	tempG.P(string(tools.GoFmt(buf.Bytes())))
	tempG.Skip()
	return genFinalG(gen, &file{origFile, tempFile, tempG})
}

// genFinalG generate final protogen.GeneratedFile
func genFinalG(gen *protogen.Plugin, f *file) (*protogen.GeneratedFile, error) {
	g := gen.NewGeneratedFile(f.orig, "")
	finaBytes, err := checkAndMerge(f)
	if err != nil {
		return nil, err
	}
	g.P(string(finaBytes))
	return g, nil
}

// generateFile generates a _errors.pb.go file containing ego errors definitions.
func generateFile(gen *protogen.Plugin, f *protogen.File, out string, mod string) (*protogen.GeneratedFile, error) {
	cmdout, err := execCmd("go", "list", out)
	if err != nil {
		return nil, fmt.Errorf("get package path fail, %w", err)
	}
	outPkg := cmdout[strings.LastIndex(cmdout, "/")+1:]
	origFile := filepath.Join(out, filepath.Base(f.GeneratedFilenamePrefix+"_test.go"))
	tempFile := genHiddenFile(origFile)
	f.GoImportPath = protogen.GoImportPath(filepath.Join(mod, string(f.GoImportPath)))
	tempG := gen.NewGeneratedFile(tempFile, f.GoImportPath)
	tempG.P("// Code generated by protoc-gen-go-test. DO NOT EDIT.")
	tempG.P()
	tempG.P("package ", outPkg)
	tempG.P()
	tempG.P("import (")
	for path, name := range importTestPackages {
		tempG.P(fmt.Sprintf("%s \"%s\"", name, path))
	}
	tempG.P(fmt.Sprintf("%s %s", f.GoPackageName, f.GoImportPath))
	tempG.P(")")
	tempG.Skip()
	generateFileContent(gen, f, tempG)
	return genFinalG(gen, &file{origFile, tempFile, tempG})
}

// generateFileContent generates the ego errors definitions, excluding the package statement.
func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) {
	g.P("// This is a compile-time assertion to ensure that this generated file")
	g.P("// is compatible with the ego package it is being compiled against.")
	g.P()
	index := 0
	for _, svc := range file.Services {
		if !generateTestSection(gen, file, g, svc) {
			index++
		}
	}
	// If all enums do not contain 'service', the current file is skipped
	if index == 0 {
		g.Skip()
	}
}

func generateTestSection(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, svc *protogen.Service) bool {
	var w svcWrapper
	for _, method := range svc.Methods {
		method.Desc.IsStreamingServer()
		d := &svcData{
			Name:              method.GoName,
			Time:              time.Now().Format("2006-01-02T15:04:05"),
			InType:            string(method.Input.Desc.Name()),
			OutType:           string(method.Output.Desc.Name()),
			Package:           string(file.GoPackageName),
			Service:           service{Name: svc.GoName},
			isStreamingServer: method.Desc.IsStreamingServer(),
			isStreamingClient: method.Desc.IsStreamingClient(),
		}
		w.Svcs = append(w.Svcs, d)
	}
	if len(w.Svcs) == 0 {
		return true
	}
	g.P(w.execute())
	return false
}
